package edu.ua.cs.cs515.gcj.tetris;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Synthesizer;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.JOptionPane;

/**
 * Manages the sound effects and music playback of the game.  This is a singleton because
 * the game should only have one instance of an audio manager.
 */
public class AudioManager {

	// Sound must be played in a separate thread to avoid blocking.
	private MusicThread musicThread;
	private SoundThread explosionThread;
	private SoundThread bonusThread;
	private boolean muted = false;
	private static AudioManager audioManager = new AudioManager();
	
	private long savedPosition = 0;
	
	public static AudioManager getInstance() {
		return audioManager;
	}

	// Default constructor.  Create a MusicThread object.
	private AudioManager() {
		musicThread = new MusicThread();
	}
	
	// Run the music thread
	public void startMusic() {
		if (muted) {
			return;
		}
		if (musicThread != null) {
			musicThread.setPosition(savedPosition);
			Thread t = new Thread(musicThread);
			t.start();
		}
	}
	
	// Cleanly stop the music from playing
	public void stopMusic(boolean storePosition) {
		if (musicThread != null) {
			if (storePosition) {
				this.savedPosition = musicThread.getPosition();
			} else {
				this.savedPosition = 0;
			}
			musicThread.stop();
			musicThread = new MusicThread();
		}
	}
	
	// Returns true if music is playing, false otherwise.
	public boolean isPlaying() {
		return musicThread.isPlaying();
	}
	
	// If a sound effect is not already playing, play a sound effect
	public void playExplosion() {
		if (explosionThread == null || !explosionThread.playing) {
			explosionThread = new SoundThread("/edu/ua/cs/cs515/gcj/tetris/res/explosion.wav");
			Thread t = new Thread(explosionThread);
			t.start();
		}
	}
	
	public void playBonus() {
		if (bonusThread == null || !bonusThread.playing) {
			bonusThread = new SoundThread("/edu/ua/cs/cs515/gcj/tetris/res/bonus.wav");
			Thread t = new Thread(bonusThread);
			t.start();
		}
	}
	
	public void setMuted(boolean muted, boolean execute) {
		this.muted = muted;
		if (execute) {
			if (muted) {
				stopMusic(true);
			} else {
				startMusic();
			}
		}
	}
	
	public boolean isMuted() {
		return this.muted;
	}
	
	/**
	 * Runnable class which is implemented to play midi music from the res package
	 */
	class MusicThread implements Runnable {
		
		private Sequence sequence;
		private Sequencer sequencer;
		private boolean quit = false;
		private URL musicURL = this.getClass().getResource("/edu/ua/cs/cs515/gcj/tetris/res/music.midi");
		
		// Default constuctor, create a midi sequencer
		public MusicThread() {
			createSequencer();
		}
		
		// Returns true if the sequencer is currently playing music
		public boolean isPlaying() {
			if (sequence == null) {
				return false;
			}
			return sequencer.isRunning();
		}
		
		public long getPosition() {
			return sequencer.getMicrosecondPosition();
		}
		
		public void setPosition(long position) {
			sequencer.setMicrosecondPosition(position);
		}
		
		// Create a sequencer for playing the embedded midi music
		private void createSequencer() {
			try {
				sequence = MidiSystem.getSequence(musicURL);
		        sequencer = MidiSystem.getSequencer();
		        sequencer.open();
		        sequencer.setSequence(sequence);
			} catch (MalformedURLException e) {
				e.printStackTrace();
			} catch (InvalidMidiDataException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			} catch (MidiUnavailableException e) {
				JOptionPane.showMessageDialog(null, "Sound is disabled because your computer is\nunable to play MIDI music.", "Sound unavailable", JOptionPane.INFORMATION_MESSAGE);
				e.printStackTrace();
			}
		}
		
		// Stop playing
		public void stop() {
			this.quit = true;
		}
		
		// Plays the music continuously until the quit boolean variable is set to true.
		public void run() {
			if (!sequencer.isOpen()) {
				createSequencer();
			}
			sequencer.setLoopCount(Sequencer.LOOP_CONTINUOUSLY);
			sequencer.start();
			while (sequencer.isRunning() && !quit) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			// Stop and close the sequencer
			if (quit) {
				sequencer.stop();
				sequencer.close();
			}
		}
	}
	
	/**
	 * Runnable class responsible for playing an embedded sound effect in the res package
	 *
	 */
	class SoundThread implements Runnable {
		private InputStream stream;
		boolean playing = false;
		boolean quit = false;
		String url;
		
		public SoundThread(String url) {
			this.url = url;
		}
		
		// Returns true if the sound effect is playing, false otherwise
		public boolean isPlaying() {
			return playing;
		}
		
		// Stop playing the sound effect
		public void stop() {
			this.quit = true;
		}
		
		// Play the sound effect until it has reached EOF or the quit boolean variable is
		// set to true.
		public void run() {
			stream = this.getClass().getResourceAsStream(url);
			try {
	          Clip clip = AudioSystem.getClip();
	          AudioInputStream inputStream = AudioSystem.getAudioInputStream(stream);
	          clip.open(inputStream);
	          clip.start(); 
	          playing = true;
	          while(clip.isActive() && !quit) {
	        	  try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
	          }
			} catch (IOException ex) {
				ex.printStackTrace();
			} catch (LineUnavailableException ex) {
				ex.printStackTrace();
			} catch (UnsupportedAudioFileException ex) {
				ex.printStackTrace();
			}
	        playing = false;
		}
		
	}
	
}